Day 10,筆者提到了 PSR 規範列表中,目前正運作的有 13 個,而 Day 11,筆者介紹了 PHP 程式碼風格 PSR-1, 12 及 Day 12 的 PHP 自動載入機制 PSR-4。而本篇是本系列最後一篇關於 PSR 的文章,介紹的是和 HTTP 通訊處理和伺服器端相關的規範如下:
編號 | 名稱 | 名稱 (中) | 簡介 |
---|---|---|---|
7 | HTTP message interfaces | HTTP 通訊介面 | HTTP 訊息規範於 RFC 7230 及 RFC 7231,URL 規範於 RFC 3986 的實作建議。 |
15 | HTTP Handlers | HTTP 處理程式 | HTTP 伺服器端請求處理程式,為 PSR-7 提供了中介層 (middleware) 的實作規範 |
17 | HTTP Factories | HTTP 工廠 | 為 PSR-7 的工廠模式。 |
PSR 已經是現代先進框架 (Modern framework) 的共同標準了,瞭解了這些等於同時懂了好幾套框架對於 HTTP 的處理方法,例如 Laravel、Zend、Slim 等等。差別在有些框架會加料,除了介面定義的方法外,還額外自定框架所需的方法,但基本方法都通用。
如果專案作品中含有對 HTTP 處理,最好的實踐是相容於 PSR-7,利用 PSR-7 包裝的抽象方法來處理,以避免使用 PHP 原生函式可能會產生的衝突問題。這也是筆者在這一系列文章中特別提到此篇文章主題的原因,畢竟要發表作品給大眾使用,總是要多方事先考慮到可能發生問題的情況才對。
不同於之前筆者在介紹 PSR 時採用翻譯原文並整理內容的方式,在此篇僅列出如何使用以及注意的細節。
PSR-7 是給想造輪子的開發者有個標準可依循,以相容同樣採用此規範的框架。其它採用 PSR-7 的套件也能彼此相容。
介面名稱 | 繼承 | 方法數 | 簡介 |
---|---|---|---|
MessageInterface (A) | - | 11 | HTTP 的通訊由客戶端的請求和伺服器端的回應所組成。此界面定義了通用的方法。(RFC 7230, RFC 7231) |
RequestInterface (B) | (A) | 6 | 表示一個對外發出的客戶端 HTTP 請求。 |
ServerRequestInterface | (B) | 13 | 表示一個接收進來的伺服器端 HTTP 請求。 |
ResponseInterface | (A) | 3 | 表示一個對外發出的伺服器端 HTTP 回應。 |
StreamInterface | - | 15 | 將資料流處理包裝成一個實例物件,並提供抽象方法來操作資料流。 |
UriInterface | - | 16 | 依 RFC 3986 來把 URI 處理包裝成實例物件,並提供抽象方法來操作 URI,主要使用範圍在 HTTP 請求,但也可以用其它地方。 |
UploadedFileInterface | - | 6 | 把檔案上傳的超全域變數 $_FILES 包裝成物件,並提供抽象方法來操作檔案。 |
現有的 PSR-7 實作套件如下:
供應商命名空間 | GiHub 專案連結 | 授權條款 | 簡介 |
---|---|---|---|
GuzzleHttp | https://github.com/guzzle/psr7 | MIT | 最早實作 PSR-7 的套件。 |
Slim | https://github.com/slimphp/Slim-Psr7 | MIT | Slim 4 框架所用的 PSR-7 套件。 |
Shieldon | https://github.com/terrylinooo/psr-http | MIT | 使用於 Shieldon WAF (web application firewall) 的 PSR-7 套件。 |
其中 Shieldon PSR-HTTP 這個套件是筆者實作,已經有現成的套件可使用的話理當不需要重造輪子,但為了用在自己的作品 Shieldon 中,仔細的審閱了各個 PSR-7 套件,讓我有了重造輪子的念頭,理由如下:
因此筆者按照 PSR-7 規範,詳讀研究 RFC 7230,RFC 7231,RFC 3986 規範文件後實作。結構如下:
上圖為 PSR-7 各介面定義的基本方法。大部份的 PHP 框架除了這些基本方法以外,還會自訂其它方法以供框架使用。但基本方法在各框架都是通用的。
在 PSR-7 規範問世之後,各路高手紛紛使用 PSR-7 大展身手,結果變成在中介層 (middleware) 的應用上出現了好幾套不同的設計模式,不同的框架在中介層的類別無法直接套用到其它框架,複用性不是很理想。為了解決這種情況,於是 PSR-15 規範出兩個介面,RequestHandlerInterface
及 MiddlewareInterface
。
RequestHandlerInterface
interface RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface;
}
MiddlewareInterface
interface MiddlewareInterface
{
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler
): ResponseInterface;
}
一般而言,PHP 框架皆已內建 RequestHandler,所以只要是專案作品是會處理到 HTTP 請求的部分,皆從中介層下手。
範例:
class RejectNoUseragent implements MiddlewareInterface
{
/**
* Invoker.
*
* @param ServerRequestInterface $request The PSR-7 server request.
* @param RequestHandlerInterface $handler The PSR-15 request handler.
*
* @return ResponseInterface
*/
public function process(
ServerRequestInterface $request,
RequestHandlerInterface $handler): ResponseInterface
{
if (!$request->hasHeader('user-agent')) {
return (new Response)->withStatus(400);
}
return $handler->handle($request);
}
}
這是一個簡易版的中介層範例,用途為:進來的 HTTP 請求沒有 User-Agent
資訊,則回用 400 (Bad Request) 給客戶端。
$handler->handle($request);
如果有含 User-Agent
,那就繼續下一個中介層。
由於創建 PSR-7 中的各類別實例化物件的時候,要另外寫程式碼帶參數,免不了寫多行程式。這種情況很適合使用筆者在 Day 7 介紹的工廠模式 來創造物件。
以下是取自 Shieldon PSR-HTTP 的範例:
$serverRequest = ServerRequestFactory::fromGlobal();
建立一個 serverRequest 的物件。
$response = ResponseFactory::fromNew();
建立一個 response 的物件。
如此一來很方便。要注意的是 PSR-15 定義的介面方法太陽春,以致於各家 PSR-15 實作,參數都不一樣!
因此都還得另外定義方法(大部分都是靜態方法 fromGlobal
)。因此在採用 PSR-15 工廠時,注意一下套件供應商的文件,看看使用方法。
由於 PSR-7 已經將超全域變數 $_GET
,$_COOKIE
, $_POST
, $_FILES
封裝進 ServerRequest
物件中使用。不得再直接對這些超全域變數進行操作。
不得使用像是 session_start、setcookie 這類函式,如圖:
正確的流程是組裝完 Response
物件後,由解析器統一輸出,把設定 Cookie 等操作集中在輸出前由 header()
函式行 header 輸出。使用 session_start
、setcookie
的同時,就提前輸出 header 了,破壞流程往後會有意想不到的問題得抓漏。
Session 的使用須棄用 PHP 原生的 $_SESSION
及相關函式,改用自定的 session driver。相信各框架皆以內建。所以別再用原生的 Session 囉。
當我們開源的作品要盡可能相容於各先進框架時:
終於結束了一些前置性介紹觀念的文章,明天開始就要開始我們的第一個 Composer 套件實作流程囉。我們明天見囉!
本文同步更新於 TerryL 部落格 Day 13 - PHP HTTP 通訊相關介面:PSR-7, PSR-15, PSR-17,歡迎前往討論。